﻿using HtmlAgilityPack;
using Ionic.Zip;
using Pdf2epub.ExtractStrategy;
using Pdf2epub.Interfaces;
using Pdf2epub.Model;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;

namespace Pdf2epub.BusinessLogic.Extractors
{
    internal sealed class GalileoOpenBookExtractor : ExtractorBase, IDisposable
    {
        private readonly List<ContentBlock> m_ContentBlockList = new List<ContentBlock>();

        private readonly FontUsageCollection m_FontUsageCollection = new FontUsageCollection();

        private readonly string m_ImagePath;

        private readonly ZipEntry[] m_ZipEntries;

        private readonly ZipFile m_ZipFile;

        private readonly string m_ZipFilePath;

        //private Chapter m_CurrentChapter = new Chapter("Unknown");

        private ContentBlock m_CurrentContentBlock;

        public GalileoOpenBookExtractor(IProjectContext projectContext, string zipFilePath)
            : base(projectContext)
        {
            m_ZipFilePath = zipFilePath;

            m_ZipFile = ZipFile.Read(m_ZipFilePath);
            m_ZipEntries = m_ZipFile.Entries.Where(entry =>
            {
                string extension = Path.GetExtension(entry.FileName);
                return extension != null && (extension.IndexOf(".htm", StringComparison.InvariantCultureIgnoreCase) != -1);
            }).ToArray();

            ExtractDocumentOutline();

            m_ImagePath = ProjectContext.ProjectConfiguration.ImageOutputPath;
        }

        internal delegate bool TestDelegate(XmlNode node);

        public FontUsageCollection FontUsages
        {
            get { return m_FontUsageCollection; }
        }

        public override int NumberOfPages
        {
            get { return m_ZipEntries.Length; }
        }

        public static XmlElement CreateXmlNodeFromHtml(string htmlContent)
        {
            try
            {
                XmlDocument xmlDoc = new XmlDocument { InnerXml = htmlContent };
                XmlElement mainTextArea = ExtractMainTextArea(xmlDoc);
                if (mainTextArea == null)
                    return null;

                RemoveContactForm(mainTextArea);

                return mainTextArea;
            }
            catch (XmlException xex)
            {
            }
            return null;
        }

        public static XmlNode FindFirstNodeBelow(XmlNodeList childNodes, TestDelegate test)
        {
            foreach (XmlNode node in childNodes)
            {
                if (test.Invoke(node))
                {
                    return node;
                }

                XmlNode subElement = FindFirstNodeBelow(node.ChildNodes, test);
                if (subElement != null)
                {
                    return subElement;
                }
            }
            return null;
        }

        public static IEnumerable<XmlNode> FindNodesBelow(XmlNodeList childNodes, TestDelegate test)
        {
            return childNodes.Cast<XmlNode>().Where(node => test(node));
        }

        public static string GetZipFileTextContent(ZipEntry zipEntry)
        {
            string tempDir = Path.GetTempPath();
            string htmlFileName = Path.Combine(tempDir, zipEntry.FileName);
            string xmlFileName = Path.ChangeExtension(htmlFileName, "xml");

            if (File.Exists(htmlFileName))
                File.Delete(htmlFileName);
            if (File.Exists(xmlFileName))
                File.Delete(xmlFileName);

            zipEntry.Extract(tempDir);

            HtmlDocument doc = new HtmlDocument();
            doc.Load(htmlFileName, Encoding.UTF7);
            doc.OptionOutputAsXml = true;
            doc.Save(xmlFileName);

            string txt;
            using (StreamReader reader = new StreamReader(xmlFileName, Encoding.UTF7))
            {
                txt = reader.ReadToEnd();
            }

            File.Delete(htmlFileName);
            File.Delete(xmlFileName);
            return txt;
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public override Chapter[] ExtractDocumentOutline()
        {
            GalileoOpenBookOutlineExtraction outlineDetection = new GalileoOpenBookOutlineExtraction();
            return outlineDetection.ExtractChapters(m_ZipEntries);
        }

        protected override ContentBlock[] Extract(int page)
        {
            page--;

            m_ContentBlockList.Clear();

            if ((page < 0) || (page >= m_ZipEntries.Length))
                return new ContentBlock[] { };

            ZipEntry zipEntry = m_ZipEntries[page];

            if (ShouldBeIgnored(zipEntry))
                return new ContentBlock[] { };

            string fileContent = GetZipFileTextContent(zipEntry);
            if (zipEntry.FileName.Contains("index.htm"))
            {
                ExtractCoverImage(fileContent);
            }
            else
            {
                ProcessFileContent(fileContent);
            }
            return m_ContentBlockList.ToArray();
        }

        private static XmlElement ExtractMainTextArea(XmlDocument htmlDoc)
        {
            var mainDiv = FindFirstNodeBelow(htmlDoc.ChildNodes, TestFindMainDiv);
            return mainDiv as XmlElement;
        }

        private static string GetStyleName(XmlElement node)
        {
            StringBuilder styleName = new StringBuilder();
            while (node != null)
            {
                if (node.HasAttribute("class"))
                {
                    styleName = styleName.Insert(0, string.Format("{0}, ", node.GetAttribute("class")));
                }
                node = node.ParentNode as XmlElement;
            }
            return styleName.ToString().Trim().Trim(',');
        }

        private static bool RecurseRequired(XmlNode node)
        {
            return (!node.Name.Equals("table", StringComparison.InvariantCultureIgnoreCase));
        }

        private static void RemoveContactForm(XmlElement mainTextArea)
        {
            XmlNode formsAnchorNode = FindFirstNodeBelow(mainTextArea.ChildNodes, TestFormArea);

            XmlNode parentNode = formsAnchorNode.ParentNode;
            if (parentNode == null)
                return;

            while (parentNode.LastChild != formsAnchorNode)
            {
                parentNode.RemoveChild(parentNode.LastChild);
            }
            parentNode.RemoveChild(formsAnchorNode);
        }

        private static bool TestFindMainDiv(XmlNode node)
        {
            if (node == null)
                return false;

            if (!node.Name.Equals("div", StringComparison.InvariantCultureIgnoreCase))
                return false;

            XmlElement element = node as XmlElement;
            if (element == null)
                return false;

            string classAttribute = element.GetAttribute("class");
            if (string.IsNullOrEmpty(classAttribute))
                return false;

            return classAttribute.Equals("main", StringComparison.InvariantCultureIgnoreCase);
        }

        private static bool TestFormArea(XmlNode node)
        {
            if (node == null)
                return false;

            if (!node.Name.Equals("a", StringComparison.InvariantCultureIgnoreCase))
                return false;

            XmlElement element = node as XmlElement;
            if (element == null)
                return false;

            string classAttribute = element.GetAttribute("name");
            if (string.IsNullOrEmpty(classAttribute))
                return false;

            return classAttribute.Equals("kommentar", StringComparison.InvariantCultureIgnoreCase);
        }

        private void CreateImageContentBlock(XmlElement elementNode)
        {
            string imageFileNameInHtml = elementNode.GetAttribute("src");
            if (ImageShouldBeIgnored(imageFileNameInHtml))
                return;

            string imgFileName = Path.GetFileName(imageFileNameInHtml);

            if (imgFileName != null)
            {
                string bigImageFileName = imgFileName.Replace("klein", "");

                string foundImageName = bigImageFileName;
                ZipEntry imageZip = FindImageZipEntry(bigImageFileName);
                if (imageZip == null)
                {
                    imageZip = FindImageZipEntry(imgFileName);
                    foundImageName = imgFileName;
                }

                if (imageZip == null)
                    return;

                string tempPath = Path.GetTempPath();
                string extractedImageFile = Path.Combine(tempPath, imageZip.FileName);
                string imageTargetFileName = Path.Combine(m_ImagePath, foundImageName);

                if (!Directory.Exists(m_ImagePath))
                    Directory.CreateDirectory(m_ImagePath);

                if (!File.Exists(imageTargetFileName))
                {
                    imageZip.Extract(tempPath, ExtractExistingFileAction.OverwriteSilently);
                    File.Move(extractedImageFile, imageTargetFileName);
                }

                ContentBlock imageContentBlock = new ContentBlock(ProjectContext, ContentType.Image, foundImageName);
                m_ContentBlockList.Add(imageContentBlock);
            }
        }

        private void CreateLineBreak()
        {
            if (m_CurrentContentBlock != null)
            {
                m_CurrentContentBlock.Content = string.Format("{0}\n", m_CurrentContentBlock.Content);
            }
        }

        private void CreateListBlock(XmlElement elementNode)
        {
            SaveCurrentBlock();

            if (m_CurrentContentBlock == null)
            {
                SaveCurrentBlock();

                FontUsage ulFont = GetFontUsageForTextNode(elementNode);
                m_CurrentContentBlock = new ContentBlock(ProjectContext, ContentType.ListBlock, ulFont);
            }
        }

        private void CreateListElement(XmlElement elementNode)
        {
            if ((m_CurrentContentBlock != null) && (m_CurrentContentBlock.ContentType != ContentType.ListBlock))
            {
                SaveCurrentBlock();
            }

            if (m_CurrentContentBlock == null)
            {
                FontUsage ulFont = GetFontUsageForTextNode(elementNode);
                m_CurrentContentBlock = new ContentBlock(ProjectContext, ContentType.ListBlock, ulFont);
            }

            FontUsage ilFont = GetFontUsageForTextNode(elementNode);
            m_CurrentContentBlock.SubBlocks.Add(new ContentBlock(ProjectContext, ContentType.ListElement, elementNode.Value, ilFont));
        }

        private void CreateTableBlock(XmlElement elementNode)
        {
            SaveCurrentBlock();

            ContentBlock table = new ContentBlock(ProjectContext, ContentType.Table);
            foreach (XmlElement tableElement in elementNode.ChildNodes)
            {
                switch (tableElement.Name)
                {
                    case "caption":
                        ProcessTableCaption(table, tableElement);
                        break;

                    case "tbody":
                        ProcessTableBody(table, tableElement);
                        break;
                }
            }

            m_ContentBlockList.Add(table);
        }

        private void CreateTextContentBlock(XmlElement elementNode)
        {
            SaveCurrentBlock();

            string fontStyle = GetStyleName(elementNode);
            FontUsage fontUsage = GetFontUsageForStyle(fontStyle);

            m_CurrentContentBlock = new ContentBlock(ProjectContext, ContentType.Text, fontUsage);
        }

        private void ExtractCoverImage(string htmlContent)
        {
            XmlDocument xmlDoc = new XmlDocument { InnerXml = htmlContent };

            XmlNode coverImageNode = FindFirstNodeBelow(xmlDoc.ChildNodes, TestFindCoverImageTag);

            XmlElement coverImageElement = coverImageNode as XmlElement;
            if (coverImageElement == null)
                return;

            string imageSrc = coverImageElement.GetAttribute("src");

            string imageFileNoExt = Path.GetFileNameWithoutExtension(imageSrc);
            if (string.IsNullOrEmpty(imageFileNoExt))
                return;

            ZipEntry imageEntry = m_ZipFile.Entries.FirstOrDefault(entry => entry.FileName.Contains(imageFileNoExt));
            if (imageEntry == null)
                return;

            string tempDir = Path.GetTempPath();
            imageEntry.Extract(tempDir, ExtractExistingFileAction.OverwriteSilently);

            string sourcePath = Path.Combine(tempDir, imageEntry.FileName);
            string targetPath = Path.Combine(m_ImagePath, "cover.jpg");

            if (!Directory.Exists(m_ImagePath))
                Directory.CreateDirectory(m_ImagePath);

            if (File.Exists(targetPath))
                File.Delete(targetPath);
            File.Move(sourcePath, targetPath);

            ProjectContext.ProjectConfiguration.CoverImagePath = "cover.jpg";

            ContentBlock coverBlock = new ContentBlock(ProjectContext, ContentType.Image, "cover.jpg") { SourcePage = 1 };

            m_ContentBlockList.Insert(0, coverBlock);
        }

        private ZipEntry FindImageZipEntry(string imageFileName)
        {
            return m_ZipFile.Entries.FirstOrDefault(entry =>
            {
                string fileName = Path.GetFileName(entry.FileName);
                return fileName != null && fileName.Equals(imageFileName);
            });
        }

        private FontUsage GetFontUsageForStyle(string fontStyle)
        {
            if (!m_FontUsageCollection.Contains(fontStyle, 0))
            {
                FontUsage font = new FontUsage(fontStyle, 0);
                m_FontUsageCollection.Add(font);
            }

            FontUsage fontUsage = m_FontUsageCollection.Get(fontStyle, 0);
            fontUsage.UsageCount++;

            return fontUsage;
        }

        private FontUsage GetFontUsageForTextNode(XmlNode xmlText)
        {
            XmlElement xmlElement = GetParentXmlElement(xmlText);
            string style = GetStyleName(xmlElement);
            FontUsage font = GetFontUsageForStyle(style);
            return font;
        }

        private XmlElement GetParentXmlElement(XmlNode xmlText)
        {
            XmlNode xmlNode = xmlText;
            while ((xmlNode != null) && !(xmlNode is XmlElement))
            {
                xmlNode = xmlNode.ParentNode;
                if (xmlNode is XmlElement)
                    break;
            }
            return xmlNode as XmlElement;
        }

        private bool ImageShouldBeIgnored(string imageFileNameInHtml)
        {
            if (imageFileNameInHtml.StartsWith("common", StringComparison.InvariantCultureIgnoreCase))
                return true;
            return false;
        }

        private void ProcessFileContent(string htmlContent)
        {
            XmlElement xmlMainTextArea = CreateXmlNodeFromHtml(htmlContent);
            if (xmlMainTextArea == null)
                return;

            ProcessNodeCollection(xmlMainTextArea.ChildNodes);
        }

        private void ProcessNodeCollection(XmlNodeList nodes)
        {
            foreach (XmlNode node in nodes)
            {
                if (node is XmlElement)
                {
                    ProcessXmlElement(node as XmlElement);
                }
                else if (node is XmlText)
                {
                    ProcessXmlText(node as XmlText);
                }

                if (RecurseRequired(node))
                {
                    ProcessNodeCollection(node.ChildNodes);
                }
            }
        }

        private void ProcessTableBody(ContentBlock table, XmlElement tableBodyElement)
        {
            foreach (XmlElement rowNode in tableBodyElement.ChildNodes)
            {
                switch (rowNode.Name)
                {
                    case "tr":
                        ProcessTableRow(table, rowNode);
                        break;
                }
            }
        }

        private void ProcessTableCaption(ContentBlock table, XmlElement captionElement)
        {
            string style = GetStyleName(captionElement);
            FontUsage fontUsageForStyle = GetFontUsageForStyle(style);

            ContentBlock captionBlock = new ContentBlock(ProjectContext, ContentType.TableCaption, captionElement.InnerText, fontUsageForStyle);
            table.SubBlocks.Add(captionBlock);
        }

        private void ProcessTableCell(ContentBlock rowBlock, XmlElement cell)
        {
            string style = GetStyleName(cell);
            FontUsage fontUsageForStyle = GetFontUsageForStyle(style);

            ContentBlock cellBlock = new ContentBlock(ProjectContext, ContentType.TableCell, cell.InnerText.Trim(), fontUsageForStyle);

            rowBlock.SubBlocks.Add(cellBlock);
        }

        private void ProcessTableRow(ContentBlock table, XmlElement rowNode)
        {
            string style = GetStyleName(rowNode);
            FontUsage fontUsageForStyle = GetFontUsageForStyle(style);

            ContentBlock rowBlock = new ContentBlock(ProjectContext, ContentType.TableRow, fontUsageForStyle);

            foreach (XmlElement cell in rowNode.ChildNodes)
            {
                switch (cell.Name)
                {
                    case "td":
                        ProcessTableCell(rowBlock, cell);
                        break;
                }
            }
            table.SubBlocks.Add(rowBlock);
        }

        private void ProcessXmlElement(XmlElement elementNode)
        {
            if (elementNode == null)
                return;

            switch (elementNode.Name)
            {
                case "h1":
                case "h2":
                case "h3":
                case "h4":
                case "h5":
                case "h6":
                case "p":
                case "pre":
                    CreateTextContentBlock(elementNode);

                    break;

                case "ul":
                    CreateListBlock(elementNode);
                    break;

                case "li":
                    CreateListElement(elementNode);
                    break;

                case "img":
                    CreateImageContentBlock(elementNode);
                    break;

                case "br":
                    CreateLineBreak();
                    break;

                case "table":
                    CreateTableBlock(elementNode);
                    break;
            }
        }

        private void ProcessXmlText(XmlText xmlText)
        {
            if (xmlText == null)
                return;

            m_CurrentContentBlock.Content = string.Format("{0} {1}", m_CurrentContentBlock.Content, xmlText.Value).TrimStart();
        }

        private void SaveCurrentBlock()
        {
            if (m_CurrentContentBlock != null)
            {
                m_ContentBlockList.Add(m_CurrentContentBlock);
                m_CurrentContentBlock = null;
            }
        }

        private bool ShouldBeIgnored(ZipEntry zipEntry)
        {
            return false;
        }

        private bool TestFindCoverImageTag(XmlNode node)
        {
            if (node == null)
                return false;

            if (!node.Name.Equals("img", StringComparison.InvariantCultureIgnoreCase))
                return false;

            XmlElement element = node as XmlElement;
            if (element == null)
                return false;

            string srcAttribute = element.GetAttribute("src");
            string altAttribute = element.GetAttribute("alt");

            if (string.IsNullOrEmpty(srcAttribute))
                return false;

            bool inCommonImageDir = srcAttribute.Contains("common");
            bool isBuchAltText = altAttribute.Contains("Buch");

            return inCommonImageDir && isBuchAltText;
        }
    }
}